/*
MIT License

Copyright (c) 2021 МГТУ им. Н.Э. Баумана, кафедра ИУ-6, Михаил Фетисов,

https://bmstu.codes/lsx/simodo/loom
*/

#include "simodo/interpret/Interpret.h"
#include "simodo/module/ModuleManagement.h"
#include "simodo/module/CodeSupplier.h"
#include "simodo/module/HardModuleLoader.h"
#include "simodo/module/SoftModule.h"
#include "simodo/inout/convert/functions.h"
#include "simodo/bormental/DrBormental.h"
#include "simodo/inout/format/fmt.h"

#include <cassert>

#if __cplusplus >= __cpp_2017
#include <filesystem>
namespace fs = std::filesystem;
#else
#include <experimental/filesystem>
namespace fs = std::filesystem::experimental;
#endif

#include <boost/dll/import.hpp>

namespace dll = boost::dll;

#define CATCH_ERRORS

namespace simodo::module
{
    ModuleManagement::ModuleManagement( inout::Reporter_abstract & m, 
                         inout::InputStreamSupplier_interface & stream_supplier,
                         std::vector<std::string> interpret_places,
                         std::vector<std::string> hard_module_places,
                         std::vector<std::string> grammar_places,
                         interpret::InterpretType interpret_type,
                         interpret::SemanticDataCollector_interface & semantic_data,
                         std::ostream & tout,
                         std::string initial_contracts_file,
                         bool debug_mode,
                         uint64_t timeout,
                         bool resume,
                         bool need_full_debug_info,
                         const std::vector<interpret::BreakPoint> & breakpoints)
        : _m(m)
        , _stream_supplier(stream_supplier)
        , _semantics_places(interpret_places)
        , _hard_module_places(hard_module_places)
        , _grammar_places(grammar_places)
        , _interpret_type(interpret_type)
        , _semantic_data(semantic_data)
        , _tout(tout)
        , _initial_contracts_file(initial_contracts_file)
        , _debug_mode(debug_mode)
        , _timeout(timeout)
        , _resume(resume)
        , _need_full_debug_info(need_full_debug_info)
        , _breakpoints(breakpoints)
        , _loom(0)
        , _hard_module_loader(hard_module_places)
    {
        if (debug_mode)
            _loom.setDebugCondition(&_debug_condition);

        _code_generator = std::make_unique<CodeSupplier>(_m,_grammar_places,_stream_supplier,_semantic_data);
        loadFactories();
    }

    void ModuleManagement::addSemanticFactories(std::vector<std::shared_ptr<interpret::SemanticModuleFactory_interface>> factories)
    {
        _semantic_factories.insert(_semantic_factories.end(),factories.begin(),factories.end());
    }

    bool ModuleManagement::execute(const std::string & module_name, std::vector<std::string> preload_module_names)
    {
        if (_semantic_factories.empty()) {
            _m.reportFatal({}, inout::fmt("Семантики не заданы"));
            return false;
        }

        interpret::Interpret inter(_interpret_type, _m, _loom, _semantic_factories, _debug_mode);
        bool                 ok = true;

        interpret::SemanticModules hosts;
        for(const std::shared_ptr<interpret::SemanticModuleFactory_interface> & f : _semantic_factories) {
            interpret::SemanticModule_interface * h = f->create(inter);
            if (h)
                hosts.push_back(h);
            else {
                ok = false;
                _m.reportFatal({}, inout::fmt("Не удалось создать семантику"));
                break;
            }
        }

        if (ok) {
            assert(&(inter.stack()));

            inter.stack().startBoundary(interpret::BoundScope::Global, interpret::BOUNDARY_MARK_MODULE_FRAME);

            for(const std::string & module_name : preload_module_names) {
                std::shared_ptr<variable::Module_interface> mod = _hard_module_loader.load(module_name, &inter);

                if(!mod) {
                    ok = false;
                    _m.reportFatal({}, inout::fmt("Не удалось загрузить модуль %1").arg(module_name));
                    break;
                }

                /// \todo Пересмотреть странную передачу самого себя в свой же метод.
                /// PVS Studio: warn V678 An object is used as an argument to its own method.
                /// Consider checking the first actual argument of the 'instantiate' function.
                inter.stack().push({ inout::toU16(module_name), mod->instantiate(mod), inout::null_token_location,
                                    {{  {variable::SPEC_ORIGIN, variable::SPEC_ORIGIN_MODULE},
                                        {variable::SPEC_BUILTIN, true},
                                    }} });

                inter.stack().addGlobal(inter.stack().top());
            }
        }

        if (!ok) {
            for(interpret::SemanticModule_interface * h : hosts)
                delete h;
            return false;
        }

        inter.instantiateSemantics(hosts);
        inter.addBreakPoints(_breakpoints);

        if (!_initial_contracts_file.empty())
            ok = execute(inter, _initial_contracts_file);

        if (ok) {
            inter.stack().startBoundary(interpret::BoundScope::Global, interpret::BOUNDARY_MARK_MODULE_FRAME);

            ok = execute(inter, module_name);
        }

        _files = inter.files();
        _loom.finish();

        return ok;
    }

    bool ModuleManagement::isTheModuleLoaded(const std::string & module_name)
    {
        return _modules.find(module_name) != _modules.end();
    }

    const ast::Node * ModuleManagement::getCode(const std::string & module_name, const inout::uri_set_t & files)
    {
        _last_error = "";
        const ast::Node * node = _code_generator->getCode(module_name, files);

        if (node == nullptr && !_code_generator->last_error().empty())
            _last_error = _code_generator->last_error();
 
        return node;
    }

    std::shared_ptr<variable::Module_interface> ModuleManagement::registerSoftModule(const std::string & module_name, std::shared_ptr<variable::Object> module)
    {
        auto it = _modules.find(module_name);
        if (it != _modules.end())
            return std::shared_ptr<variable::Module_interface>();

        auto [it_new, ok] = _modules.insert({module_name, std::make_shared<SoftModule>(module)});
        if (!ok)
            return std::shared_ptr<variable::Module_interface>();

        return it_new->second;
    }

    std::shared_ptr<variable::Object> ModuleManagement::produceObject(const std::string & module_name,
                                interpret::Interpret_interface * interpret)
    {
        _last_error = "";
        auto it = _modules.find(module_name);

        if (it != _modules.end())
            return std::make_shared<variable::Object>(it->second->instantiate(it->second));

        std::shared_ptr<variable::Module_interface> module;
        fs::path                                    path_to_module {module_name};
        std::string                                 extension {path_to_module.extension().string()};

        if (extension.empty() || extension == ".") {
            HardModuleLoader hard_module_loader(_hard_module_places);

            module = hard_module_loader.load(module_name,interpret);

            if (!module)
                _last_error = hard_module_loader.last_error();
        }
        else {
            /// \note Загрузка мягких модулей выполняется в интерпретаторе, т.к. для
            /// этого необходимо использовать Interpret. Строго говоря, если мы попали сюда, 
            /// значит что-то пошло не так.
            assert(false);
        }

        if (!module)
            return std::shared_ptr<variable::Object>();

        version_t v = module->version();
        if (v.major() != LibVersion_Major || v.minor() > LibVersion_Minor) {
            _last_error = inout::fmt("Incorrect version of module '%1'").arg(module_name);
            return std::shared_ptr<variable::Object>();
        }

        _modules.insert({module_name, module});

        /// \todo Пересмотреть странную передачу самого себя в свой же метод.
        /// PVS Studio: warn V678 An object is used as an argument to its own method.
        /// Consider checking the first actual argument of the 'instantiate' function.
        return std::make_shared<variable::Object>(module->instantiate(module));
    }

    void ModuleManagement::loadFactories()
    {
        for(const std::string & interpret_folder_name : _semantics_places) 
            if (!interpret_folder_name.empty()) {
                fs::path path_to_interpret_folder { interpret_folder_name };

                for (auto const & f : fs::directory_iterator{path_to_interpret_folder}) 
                    if (f.path().extension() == ".simodo-semantics") {
                        std::function<InterpretFactory_t> creator = nullptr;

#ifdef CATCH_ERRORS
                        try {
#endif
                            creator = dll::import_alias<InterpretFactory_t>(f.path().string().c_str(),"create_simodo_interpret");
#ifdef CATCH_ERRORS
                        }
                        catch(std::runtime_error & e) {
                            throw bormental::DrBormental("ModuleManagement::loadFactories", e.what());
                        }
#endif
                        _semantic_factories.push_back(creator(*this));
                    }
            }
    }

    bool ModuleManagement::execute(interpret::Interpret_interface & inter, const std::string & file_name)
    {
        inter.addFile(file_name);

        const ast::Node * code = getCode(file_name, inter.files());

        if (!code) {
            if (!_last_error.empty())
                _m.reportError(file_name, _last_error);
            return false;
        }

        if (!_debug_mode)
            return inter.execute(*code);

        inter.addFlowLayer(*code);
        _loom.stretch(inter.fiber());

        return debug(inter.files());
    }
}
